import ezdxf
import json
import os
from shapely.geometry import Polygon
from shapely.geometry import Polygon, Point
from itertools import combinations
import math
import pandas as pd
import ast
import numpy as np
from itertools import combinations
from sklearn.metrics.pairwise import cosine_similarity
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import os

# --------------1. 提取 dxf中的图元信息------------------
# ---------- 判断是否为矩形 ----------
def is_rectangle(points, tolerance=1e-3):
    if len(points) != 4:
        return False
    polygon = Polygon(points)
    if not polygon.is_valid or not polygon.is_simple:
        return False
    angles = []
    for i in range(4):
        a, b, c = points[i], points[(i + 1) % 4], points[(i + 2) % 4]
        ab = (b[0] - a[0], b[1] - a[1])
        bc = (c[0] - b[0], c[1] - b[1])
        dot = ab[0] * bc[0] + ab[1] * bc[1]
        norm_ab, norm_bc = math.hypot(*ab), math.hypot(*bc)
        if norm_ab * norm_bc == 0:
            return False
        angle = math.acos(max(-1, min(1, dot / (norm_ab * norm_bc)))) * 180 / math.pi
        angles.append(angle)
    return all(abs(angle - 90) < 2 for angle in angles)


# 识别 DXF 文件中的图元信息
def extract_shapes_from_dxf(dxf_path, output_excel):
    doc = ezdxf.readfile(dxf_path)
    msp = doc.modelspace()

    rectangles, circles, lines_list, texts = [], [], [], []

    # LWPOLYLINE 提取矩形
    rect_id = 1
    for e in msp.query('LWPOLYLINE'):
        if not e.is_closed:
            continue
        points = [(v[0], v[1]) for v in e.get_points()]
        if len(points) == 4 and is_rectangle(points):
            rect_data = {"ID": f"矩形{rect_id}"}
            for i, p in enumerate(points):
                rect_data[f"点{i + 1}_x"], rect_data[f"点{i + 1}_y"] = round(p[0], 3), round(p[1], 3)
            rectangles.append(rect_data)
            rect_id += 1

    # 直线
    lines = []
    for e in msp.query('LINE'):
        start = (round(e.dxf.start.x, 3), round(e.dxf.start.y, 3))
        end = (round(e.dxf.end.x, 3), round(e.dxf.end.y, 3))
        lines.append((start, end))

    # 仅构造闭环的四边形组合
    point_to_lines = defaultdict(list)
    for (start, end) in lines:
        point_to_lines[start].append((start, end))
        point_to_lines[end].append((start, end))

    visited_loops = set()

    def find_loop(start_point, current_point, path, depth=0):
        if depth == 4:
            return path if current_point == start_point else None
        for line in point_to_lines[current_point]:
            if line in path or line[::-1] in path:
                continue
            next_point = line[1] if line[0] == current_point else line[0]
            result = find_loop(start_point, next_point, path + [line], depth + 1)
            if result:
                return result
        return None

    for start_point in point_to_lines:
        loop = find_loop(start_point, start_point, [], 0)
        if loop:
            flat_points = [pt for seg in loop for pt in seg]
            unique_pts = list({pt for pt in flat_points})
            if len(unique_pts) == 4 and is_rectangle(unique_pts):
                rect_data = {"ID": f"矩形{rect_id}"}
                for i, p in enumerate(unique_pts):
                    rect_data[f"点{i + 1}_x"], rect_data[f"点{i + 1}_y"] = round(p[0], 3), round(p[1], 3)
                rectangles.append(rect_data)
                rect_id += 1
                visited_loops.add(tuple(sorted(loop)))

    # 圆
    circle_id = 1
    for circle in msp.query('CIRCLE'):
        center = circle.dxf.center
        radius = circle.dxf.radius
        circles.append({
            "ID": f"圆形{circle_id}",
            "center_x": round(center.x, 3),
            "center_y": round(center.y, 3),
            "radius": round(radius, 3)
        })
        circle_id += 1

    # 直线列表
    line_id = 1
    for s, e in lines:
        lines_list.append({
            "ID": f"直线{line_id}",
            "start_x": s[0],
            "start_y": s[1],
            "end_x": e[0],
            "end_y": e[1]
        })
        line_id += 1

    # 文本 TEXT
    text_id = 1
    for e in msp.query('TEXT'):
        content = e.dxf.text.strip()
        x, y = round(e.dxf.insert.x, 3), round(e.dxf.insert.y, 3)
        texts.append({
            "ID": f"文本{text_id}",
            "内容": content,
            "x": x,
            "y": y
        })
        text_id += 1

    # 写入 Excel
    with pd.ExcelWriter(output_excel, engine="openpyxl") as writer:
        pd.DataFrame(rectangles).to_excel(writer, sheet_name="矩形", index=False)
        pd.DataFrame(lines_list).to_excel(writer, sheet_name="直线", index=False)
        pd.DataFrame(circles).to_excel(writer, sheet_name="圆形", index=False)
        pd.DataFrame(texts).to_excel(writer, sheet_name="文本", index=False)

    print(
        f"✅ 提取完成，共矩形 {len(rectangles)} 个，直线 {len(lines_list)} 条，圆形 {len(circles)} 个，文本 {len(texts)} 个")

    # 构建结构化返回字典
    return {
        "矩形": {
            r["ID"]: [{"x": r[f"点{i}_x"], "y": r[f"点{i}_y"]} for i in range(1, 5)]
            for r in rectangles
        },
        "直线": {
            l["ID"]: {"start": {"x": l["start_x"], "y": l["start_y"]}, "end": {"x": l["end_x"], "y": l["end_y"]}}
            for l in lines_list
        },
        "圆形": {
            c["ID"]: {"center": {"x": c["center_x"], "y": c["center_y"]}, "radius": c["radius"]}
            for c in circles
        },
        "文本": {
            t["ID"]: {"内容": t["内容"], "x": t["x"], "y": t["y"]}
            for t in texts
        }
    }


# --------------2. 与模板设备中的信息比对、提取设备坐标------------------

# ---------- 加载模板设备 ----------
def load_template_from_excel(excel_path):
    df = pd.read_excel(excel_path)
    templates_dict = {}

    for _, row in df.iterrows():
        device_name = row["设备编号"]
        t_type = str(row["图元类型"]).strip().lower()
        coord_raw = row["坐标信息"]
        if pd.isna(t_type) or pd.isna(coord_raw):
            continue

        try:
            coord_raw = str(coord_raw).replace("，", ",")
            coord_val = ast.literal_eval(coord_raw)
        except:
            continue

        shape = {"type": t_type}
        if t_type == "矩形":
            shape["points"] = coord_val
        elif t_type == "直线":
            shape["start"] = coord_val[0]
            shape["end"] = coord_val[1]
        elif t_type == "圆形":
            shape["center"] = coord_val
            shape["radius"] = float(row.get("半径", 0))

        if device_name not in templates_dict:
            templates_dict[device_name] = []
        templates_dict[device_name].append(shape)

    templates = [
        {"name": name, "elements": elements}
        for name, elements in templates_dict.items()
    ]
    return templates


# ---------- 几何处理工具 ----------
# ---------- 将图元转换为点 ----------
def shape_to_points(shape):
    if shape["type"] == "矩形":
        return shape["points"]
    elif shape["type"] == "直线":
        return [shape["start"], shape["end"]]
    elif shape["type"] == "圆形":
        x, y = shape["center"]
        r = shape.get("radius", 0)
        return [(x + r, y), (x - r, y), (x, y + r), (x, y - r)]
    return []


# ---------- 坐标归一化 ----------
def normalize_coords(coords, epsilon=1e-6):
    xs = [p[0] if isinstance(p, tuple) else p["x"] for p in coords]
    ys = [p[1] if isinstance(p, tuple) else p["y"] for p in coords]
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)
    width = max(max_x - min_x, epsilon)
    height = max(max_y - min_y, epsilon)
    return [[(x - min_x) / width, (y - min_y) / height] for x, y in zip(xs, ys)]


# ---------- 展平 ----------
def flatten_points(points):
    return np.array(points).flatten().reshape(1, -1)


# ---------- 图元组合转向量 ----------
def shape_group_vector(shapes):
    all_points = []
    for shape in shapes:
        all_points.extend(shape_to_points(shape))
    norm = normalize_coords(all_points)
    return flatten_points(norm)


# ---------- 结构比对 ----------
def compare_group(template_shapes, candidate_shapes, threshold=0.97):
    if len(template_shapes) != len(candidate_shapes):
        return False
    for ts, cs in zip(template_shapes, candidate_shapes):
        if ts["type"] != cs["type"]:
            return False
    tpl_vec = shape_group_vector(template_shapes)
    cand_vec = shape_group_vector(candidate_shapes)
    if tpl_vec.shape[1] != cand_vec.shape[1]:
        return False
    return cosine_similarity(tpl_vec, cand_vec)[0][0] >= threshold


# ---------- 读取模板 ----------
def load_template_from_excel(excel_path):
    import pandas as pd
    import ast

    df = pd.read_excel(excel_path)
    templates_dict = {}

    for _, row in df.iterrows():
        device_name = row["设备编号"]
        t_type = str(row["图元类型"]).strip().lower()
        coord_raw = row["坐标信息"]

        if pd.isna(t_type) or (t_type != "文本" and pd.isna(coord_raw)):
            continue

        shape = {"type": t_type}

        try:
            if t_type == "矩形":
                coord_val = ast.literal_eval(str(coord_raw).replace("，", ","))
                shape["points"] = coord_val
            elif t_type == "直线":
                coord_val = ast.literal_eval(str(coord_raw).replace("，", ","))
                shape["start"] = coord_val[0]
                shape["end"] = coord_val[1]
            elif t_type == "圆形":
                coord_val = ast.literal_eval(str(coord_raw).replace("，", ","))
                shape["center"] = coord_val
                shape["radius"] = float(row.get("半径", 0))
            elif t_type == "文本":
                shape["text"] = str(row.get("文本内容", "")).strip()
        except:
            continue

        if device_name not in templates_dict:
            templates_dict[device_name] = []

        templates_dict[device_name].append(shape)

    return [{"name": k, "elements": v} for k, v in templates_dict.items()]


def merge_and_save_matched_devices(
        excel_path: str,
        dxf_json: dict,
        output_excel_path: str
):
    """
    综合两个方法：
    - match_devices_from_drawing（基于模板匹配）
    - find_devices_by_enclosed_text（基于闭合矩形 + 文本）

    输出统一格式，合并写入 Excel 的 “匹配结果” sheet。
    """
    import pandas as pd

    # ---------- 一、模板匹配 ----------
    templates = load_template_from_excel(excel_path)
    rectangles = dxf_json.get("矩形", {})
    lines = dxf_json.get("直线", {})
    texts = dxf_json.get("文本", {})

    candidate_shapes = []
    for rid, r in rectangles.items():
        candidate_shapes.append({"id": rid, "type": "矩形", "points": r})
    for lid, l in lines.items():
        candidate_shapes.append({"id": lid, "type": "直线", "start": l["start"], "end": l["end"]})

    matches = []
    for tpl in templates:
        tpl_name = tpl["name"]
        tpl_shapes = tpl["elements"]
        tpl_shape_num = len(tpl_shapes)

        matched_texts = [t for tid, t in texts.items() if tpl_name in t["内容"]]
        if not matched_texts:
            print(f"⚠️ 未找到设备编号文本: {tpl_name}")
            continue

        for t in matched_texts:
            tx, ty = t["x"], t["y"]
            for group in combinations(candidate_shapes, tpl_shape_num):
                group_shapes = list(group)
                if compare_group(tpl_shapes, group_shapes):
                    all_pts = [pt for g in group_shapes for pt in shape_to_points(g)]
                    xs = [p[0] if isinstance(p, tuple) else p["x"] for p in all_pts]
                    ys = [p[1] if isinstance(p, tuple) else p["y"] for p in all_pts]
                    matches.append({
                        "设备类型": tpl_name,
                        "图元组合": ", ".join(g["id"] for g in group_shapes),
                        "左下角坐标(x)": round(min(xs), 3),
                        "左下角坐标(y)": round(min(ys), 3),
                        "关联文本": t["内容"]
                    })
                    break

    df_template = pd.DataFrame(matches)

    # ---------- 二、闭合矩形 + 文本 匹配 ----------
    from shapely.geometry import Polygon, Point

    rectangle_matches = []
    rectangles = dxf_json.get("矩形", {})
    texts = dxf_json.get("文本", {})
    seen_texts = set()

    for rect_id, pts in rectangles.items():
        # 遍历所有已提取的矩形图元，每个矩形有唯一 ID（如“矩形3”）和四个顶点坐标列表 pts

        if len(pts) != 4:
            continue
        # 如果当前矩形不是 4 个点（不是标准矩形），则跳过（不处理）

        polygon = Polygon([(p["x"], p["y"]) for p in pts]).buffer(1e-2)
        # 用 Shapely 构造一个 Polygon 对象（多边形），用于判断文本是否落在这个矩形中。
        if not polygon.is_valid:
            continue
        # 如果构造出来的多边形不合法（比如自交），则跳过
        for tid, tinfo in texts.items():
            # 遍历所有文本图元，每个文本有唯一 ID（如“文本12”）和内容、坐标信息
            content = tinfo.get("内容", "").strip()
            # 获取文本内容，并去除前后空白字符
            if not content or content in seen_texts:
                continue
            # 如果文本为空，或之前已处理过相同内容（避免重复计入），则跳过
            point = Point(tinfo["x"], tinfo["y"])
            # 构造一个点对象，用于判断是否落在矩形内
            if polygon.contains(point):
                # 如果该文本落在矩形内部，认为该矩形+文本是一个设备
                xs = [p["x"] for p in pts]
                ys = [p["y"] for p in pts]
                # 提取矩形的所有 X/Y 坐标，用于计算左下角位置
                rectangle_matches.append({
                    "设备类型": content,  # 用文本内容作为设备名称
                    "图元组合": rect_id,  # 图元组合只有当前这个矩形
                    "左下角坐标(x)": round(min(xs), 3),  # 左下角 X 坐标（最小 X）
                    "左下角坐标(y)": round(min(ys), 3),  # 左下角 Y 坐标（最小 Y）
                    "关联文本": content  # 保留文本内容作为关联文本
                })

                seen_texts.add(content)
                # 把该文本内容加入已处理集合，避免后续重复添加

                break
                # 一个矩形中一旦找到文本就不再继续找（每个矩形只关联一个设备）

    df_rectangles = pd.DataFrame(rectangle_matches)

    # ---------- 三、合并写入 ----------
    df_all = pd.concat([df_template, df_rectangles], ignore_index=True)

    # 以“设备类型”字段为标准去重（保留第一条记录）
    df_all = df_all.drop_duplicates(subset=["设备类型"], keep="first")

    with pd.ExcelWriter(output_excel_path, engine="openpyxl", mode="a", if_sheet_exists="replace") as writer:
        df_all.to_excel(writer, sheet_name="匹配结果", index=False)

        # print(f"共保存匹配结果：{len(df_all)} 条（模板匹配 {len(df_template)} 条 + 闭合矩形 {len(df_rectangles)} 条）")
        return df_all


def add_device_to_dxf(
        dxf_file_path: str,
        match_df: pd.DataFrame,
        dxf_json: dict,
        output_path: str,
        target_device_name: str,
        insert_position: dict
):
    """
    将匹配结果中的某类设备图元插入到指定位置（左下角坐标）。

    参数说明：
    - dxf_file_path: 原始 DXF 文件路径
    - match_df: 匹配结果 DataFrame（包含“设备类型”、“图元组合”等）
    - dxf_json: extract_shapes_from_dxf 返回的结构化图元数据
    - output_path: 保存新 DXF 文件的路径
    - target_device_name: 要插入的设备类型（如 "壁挂空调"）
    - insert_position: 新设备插入位置，格式为 {"x": float, "y": float}
    """
    import ezdxf

    doc = ezdxf.readfile(dxf_file_path)
    msp = doc.modelspace()

    matched_rows = match_df[match_df["设备类型"] == target_device_name]
    if matched_rows.empty:
        print(f"未找到设备类型: {target_device_name}")
        return

    row = matched_rows.iloc[0]
    shape_ids = [s.strip() for s in row["图元组合"].split(",")]
    original_shapes = []

    for sid in shape_ids:
        if sid.startswith("矩形"):
            pts = [(p["x"], p["y"]) for p in dxf_json["矩形"].get(sid, [])]
            original_shapes.append({"type": "矩形", "points": pts})
        elif sid.startswith("直线"):
            data = dxf_json["直线"].get(sid, {})
            original_shapes.append({"type": "直线", "start": (data["start"]["x"], data["start"]["y"]),
                                    "end": (data["end"]["x"], data["end"]["y"])})
        elif sid.startswith("圆形"):
            data = dxf_json["圆形"].get(sid, {})
            original_shapes.append({"type": "圆形", "center": (data["center"]["x"], data["center"]["y"]),
                                    "radius": data["radius"]})

    all_points = []
    for s in original_shapes:
        if s["type"] == "矩形":
            all_points.extend(s["points"])
        elif s["type"] == "直线":
            all_points.extend([s["start"], s["end"]])
        elif s["type"] == "圆形":
            all_points.append(s["center"])

    xs = [pt[0] for pt in all_points]
    ys = [pt[1] for pt in all_points]
    dx = insert_position["x"] - min(xs)
    dy = insert_position["y"] - min(ys)

    for s in original_shapes:
        if s["type"] == "矩形":
            new_pts = [(x + dx, y + dy) for x, y in s["points"]]
            msp.add_lwpolyline(new_pts, close=True)
        elif s["type"] == "直线":
            new_start = (s["start"][0] + dx, s["start"][1] + dy)
            new_end = (s["end"][0] + dx, s["end"][1] + dy)
            msp.add_line(new_start, new_end)
        elif s["type"] == "圆形":
            new_center = (s["center"][0] + dx, s["center"][1] + dy)
            msp.add_circle(new_center, s["radius"])

    if "关联文本" in row and isinstance(row["关联文本"], str):
        # 计算设备图元相对于插入点的偏移中心（即图元自身的中心点）
        relative_points = []

        for sid in shape_ids:
            if sid.startswith("矩形") and sid in dxf_json["矩形"]:
                relative_points.extend([(p["x"], p["y"]) for p in dxf_json["矩形"][sid]])
            elif sid.startswith("直线") and sid in dxf_json["直线"]:
                relative_points.append((dxf_json["直线"][sid]["start"]["x"], dxf_json["直线"][sid]["start"]["y"]))
                relative_points.append((dxf_json["直线"][sid]["end"]["x"], dxf_json["直线"][sid]["end"]["y"]))
            elif sid.startswith("圆形") and sid in dxf_json["圆形"]:
                center = dxf_json["圆形"][sid]["center"]
                relative_points.append((center["x"], center["y"]))

        if relative_points:
            min_x = min(p[0] for p in relative_points)
            min_y = min(p[1] for p in relative_points)
            # 以最小点为左下角计算偏移量（相对坐标）
            relative_offsets = [(p[0] - min_x, p[1] - min_y) for p in relative_points]
            offset_x = sum(p[0] for p in relative_offsets) / len(relative_offsets)
            offset_y = sum(p[1] for p in relative_offsets) / len(relative_offsets)

            text_x = insert_position["x"] + offset_x * 0.8
            text_y = insert_position["y"] + offset_y * 0.6
            msp.add_text(row["关联文本"], dxfattribs={"height": 100}).dxf.insert = (text_x, text_y)

    doc.saveas(output_path)
    print(f"成功插入设备“{target_device_name}”并保存至 {output_path}")


def delete_device_from_dxf(
        dxf_file_path: str,
        match_df: pd.DataFrame,
        dxf_json: dict,
        output_path: str,
        target_device_name: str
):
    """
    删除 DXF 图纸中指定设备的所有图元（矩形、直线、圆形、文本）。

    参数：
    - dxf_file_path: 原始 DXF 图纸路径
    - match_df: 匹配结果 DataFrame，需包含“设备类型”和“图元组合”
    - dxf_json: 提取自 extract_shapes_from_dxf 的结构化图元信息
    - output_path: 删除设备后的 DXF 输出路径
    - target_device_name: 要删除的设备类型（对应“设备类型”列）
    """
    import ezdxf

    doc = ezdxf.readfile(dxf_file_path)
    msp = doc.modelspace()

    # 筛选匹配的设备行
    target_rows = match_df[match_df["设备类型"] == target_device_name]
    if target_rows.empty:
        print(f"未找到设备类型为“{target_device_name}”的图元，跳过删除")
        return

    # 收集所有要删除的图元ID和关联文本
    all_shape_ids = set()
    all_texts = set()

    for _, row in target_rows.iterrows():
        ids = [s.strip() for s in str(row.get("图元组合", "")).split(",")]
        all_shape_ids.update(ids)
        if "关联文本" in row and isinstance(row["关联文本"], str):
            all_texts.add(row["关联文本"].strip())

    def is_same_point(p1, p2, tol=1e-2):
        return abs(p1[0] - p2[0]) < tol and abs(p1[1] - p2[1]) < tol

    def match_polyline(entity_pts, json_pts):
        if len(entity_pts) != 4 or len(json_pts) != 4:
            return False
        return all(any(is_same_point((p["x"], p["y"]), ep) for ep in entity_pts) for p in json_pts)

    # 遍历所有图元实体，删除匹配上的
    to_delete = []
    for e in msp:
        etype = e.dxftype()
        if etype == "LWPOLYLINE":
            entity_pts = [(round(p[0], 3), round(p[1], 3)) for p in e.get_points()]
            for sid in all_shape_ids:
                if sid.startswith("矩形") and sid in dxf_json["矩形"]:
                    if match_polyline(entity_pts, dxf_json["矩形"][sid]):
                        to_delete.append(e)
                        break

        elif etype == "LINE":
            for sid in all_shape_ids:
                if sid.startswith("直线") and sid in dxf_json["直线"]:
                    j = dxf_json["直线"][sid]
                    s_match = is_same_point((e.dxf.start.x, e.dxf.start.y), (j["start"]["x"], j["start"]["y"]))
                    e_match = is_same_point((e.dxf.end.x, e.dxf.end.y), (j["end"]["x"], j["end"]["y"]))
                    if s_match and e_match:
                        to_delete.append(e)
                        break

        elif etype == "CIRCLE":
            for sid in all_shape_ids:
                if sid.startswith("圆形") and sid in dxf_json["圆形"]:
                    c = dxf_json["圆形"][sid]
                    if is_same_point((e.dxf.center.x, e.dxf.center.y), (c["center"]["x"], c["center"]["y"])):
                        to_delete.append(e)
                        break

        elif etype == "TEXT":
            if e.dxf.text.strip() in all_texts:
                to_delete.append(e)

    for entity in to_delete:
        msp.delete_entity(entity)

    doc.saveas(output_path)
    print(f" 已删除设备“{target_device_name}”的 {len(to_delete)} 个图元，保存至: {output_path}")


def move_device_in_dxf(
        dxf_file_path: str,
        match_df: pd.DataFrame,
        dxf_json: dict,
        output_path: str,
        target_device_name: str,
        new_position: dict  # e.g. {"x": 1000, "y": 800}
):
    """
    将指定设备整体平移到指定坐标位置（以设备左下角为基准）。

    参数：
    - dxf_file_path: 原始 DXF 文件路径
    - match_df: 设备匹配结果 DataFrame（包含设备类型、图元组合、关联文本）
    - dxf_json: 提取的图元信息（矩形、直线、圆形、文本）
    - output_path: 移动后 DXF 的保存路径
    - target_device_name: 要移动的设备类型
    - new_position: 设备新位置的左下角坐标（dict: {"x": float, "y": float}）
    """
    import ezdxf

    doc = ezdxf.readfile(dxf_file_path)
    msp = doc.modelspace()

    # 查找设备
    target_rows = match_df[match_df["设备类型"] == target_device_name]
    if target_rows.empty:
        print(f"未找到设备“{target_device_name}”")
        return

    # 收集图元ID与所有坐标点
    all_shape_ids = set()
    all_points = []

    for _, row in target_rows.iterrows():
        ids = [s.strip() for s in row["图元组合"].split(",")]
        all_shape_ids.update(ids)
        assoc_text = row.get("关联文本")
        for tid, tinfo in dxf_json.get("文本", {}).items():
            if tinfo.get("内容") == assoc_text:
                all_points.append({"x": tinfo["x"], "y": tinfo["y"]})

    for sid in all_shape_ids:
        if sid.startswith("矩形"):
            all_points.extend(dxf_json["矩形"].get(sid, []))
        elif sid.startswith("直线"):
            line = dxf_json["直线"].get(sid)
            if line:
                all_points.extend([line["start"], line["end"]])
        elif sid.startswith("圆形"):
            all_points.append(dxf_json["圆形"].get(sid, {}).get("center", {}))

    xs = [p["x"] for p in all_points]
    ys = [p["y"] for p in all_points]
    min_x, min_y = min(xs), min(ys)
    dx = new_position["x"] - min_x
    dy = new_position["y"] - min_y

    def is_same_point(p1, p2, tol=1e-2):
        return abs(p1[0] - p2[0]) < tol and abs(p1[1] - p2[1]) < tol

    def match_polyline(entity_pts, json_pts):
        return len(entity_pts) == 4 and all(
            any(is_same_point((p["x"], p["y"]), ep) for ep in entity_pts)
            for p in json_pts
        )

    # 平移图元
    for e in msp:
        etype = e.dxftype()
        if etype == "LWPOLYLINE":
            entity_pts = [(round(p[0], 3), round(p[1], 3)) for p in e.get_points()]
            for sid in all_shape_ids:
                if sid.startswith("矩形") and sid in dxf_json["矩形"]:
                    if match_polyline(entity_pts, dxf_json["矩形"][sid]):
                        new_pts = [(x + dx, y + dy) for x, y, *_ in e.get_points()]
                        e.set_points(new_pts)
                        break

        elif etype == "LINE":
            for sid in all_shape_ids:
                if sid.startswith("直线") and sid in dxf_json["直线"]:
                    j = dxf_json["直线"][sid]
                    s_match = is_same_point((e.dxf.start.x, e.dxf.start.y), (j["start"]["x"], j["start"]["y"]))
                    e_match = is_same_point((e.dxf.end.x, e.dxf.end.y), (j["end"]["x"], j["end"]["y"]))
                    if s_match and e_match:
                        e.dxf.start = (e.dxf.start.x + dx, e.dxf.start.y + dy)
                        e.dxf.end = (e.dxf.end.x + dx, e.dxf.end.y + dy)
                        break

        elif etype == "CIRCLE":
            for sid in all_shape_ids:
                if sid.startswith("圆形") and sid in dxf_json["圆形"]:
                    c = dxf_json["圆形"][sid]
                    if is_same_point((e.dxf.center.x, e.dxf.center.y), (c["center"]["x"], c["center"]["y"])):
                        e.dxf.center = (e.dxf.center.x + dx, e.dxf.center.y + dy)
                        break

        elif etype == "TEXT":
            for _, row in target_rows.iterrows():
                if e.dxf.text.strip() == row.get("关联文本"):
                    e.dxf.insert = (e.dxf.insert.x + dx, e.dxf.insert.y + dy)

    doc.saveas(output_path)
    print(f"已将设备“{target_device_name}”移动到坐标 ({new_position['x']}, {new_position['y']})，保存至: {output_path}")


def replace_device_in_dxf(
        dxf_file_path: str,
        match_df: pd.DataFrame,
        dxf_json: dict,
        output_path: str,
        origin_device_name: str,
        target_device_name: str
):
    """
    替换图纸中的设备，将原始设备替换为目标设备（位置保持一致）。

    参数：
    - dxf_file_path: 原始 DXF 文件路径
    - match_df: 设备匹配结果 DataFrame
    - dxf_json: 图元结构化数据
    - output_path: 输出 DXF 文件路径
    - origin_device_name: 被替换的设备名称（match_df中存在）
    - target_device_name: 用于替换的新设备名称（match_df中存在）
    """
    import ezdxf

    doc = ezdxf.readfile(dxf_file_path)
    msp = doc.modelspace()

    # 获取原始设备与目标设备信息
    origin_rows = match_df[match_df["设备类型"] == origin_device_name]
    target_rows = match_df[match_df["设备类型"] == target_device_name]

    if origin_rows.empty:
        print(f"未找到原始设备：{origin_device_name}")
        return
    if target_rows.empty:
        print(f"未找到替换设备：{target_device_name}")
        return

    origin_row = origin_rows.iloc[0]
    target_row = target_rows.iloc[0]

    origin_ids = [s.strip() for s in origin_row["图元组合"].split(",")]
    origin_text = origin_row.get("关联文本")
    insert_position = {
        "x": float(origin_row["左下角坐标(x)"]),
        "y": float(origin_row["左下角坐标(y)"])
    }

    # 删除原始图元
    def is_same_point(p1, p2, tol=1e-2):
        return abs(p1[0] - p2[0]) < tol and abs(p1[1] - p2[1]) < tol

    for e in list(msp):
        etype = e.dxftype()
        if etype == "LWPOLYLINE":
            entity_pts = [(round(p[0], 3), round(p[1], 3)) for p in e.get_points()]
            for sid in origin_ids:
                if sid.startswith("矩形") and sid in dxf_json["矩形"]:
                    json_pts = [(p["x"], p["y"]) for p in dxf_json["矩形"][sid]]
                    if len(json_pts) == 4 and all(
                            any(is_same_point(p, ep) for ep in entity_pts)
                            for p in json_pts
                    ):
                        msp.delete_entity(e)
                        break

        elif etype == "LINE":
            for sid in origin_ids:
                if sid.startswith("直线") and sid in dxf_json["直线"]:
                    j = dxf_json["直线"][sid]
                    s_match = is_same_point((e.dxf.start.x, e.dxf.start.y), (j["start"]["x"], j["start"]["y"]))
                    e_match = is_same_point((e.dxf.end.x, e.dxf.end.y), (j["end"]["x"], j["end"]["y"]))
                    if s_match and e_match:
                        msp.delete_entity(e)
                        break

        elif etype == "CIRCLE":
            for sid in origin_ids:
                if sid.startswith("圆形") and sid in dxf_json["圆形"]:
                    c = dxf_json["圆形"][sid]
                    if is_same_point((e.dxf.center.x, e.dxf.center.y), (c["center"]["x"], c["center"]["y"])):
                        msp.delete_entity(e)
                        break

        elif etype == "TEXT":
            if origin_text and e.dxf.text.strip() == origin_text:
                msp.delete_entity(e)

    # 构造 target_shapes 图元
    target_ids = [s.strip() for s in target_row["图元组合"].split(",")]
    insert_shapes = []
    for sid in target_ids:
        if sid.startswith("矩形") and sid in dxf_json["矩形"]:
            insert_shapes.append({
                "type": "矩形",
                "points": [(p["x"], p["y"]) for p in dxf_json["矩形"][sid]]
            })
        elif sid.startswith("直线") and sid in dxf_json["直线"]:
            d = dxf_json["直线"][sid]
            insert_shapes.append({
                "type": "直线",
                "start": (d["start"]["x"], d["start"]["y"]),
                "end": (d["end"]["x"], d["end"]["y"])
            })
        elif sid.startswith("圆形") and sid in dxf_json["圆形"]:
            c = dxf_json["圆形"][sid]
            insert_shapes.append({
                "type": "圆形",
                "center": (c["center"]["x"], c["center"]["y"]),
                "radius": c.get("radius", 0)
            })

    # 插入新图元
    for shape in insert_shapes:
        if shape["type"] == "矩形":
            pts = shape["points"]
            dx = insert_position["x"] - min(p[0] for p in pts)
            dy = insert_position["y"] - min(p[1] for p in pts)
            msp.add_lwpolyline([(x + dx, y + dy) for x, y in pts], close=True)

        elif shape["type"] == "直线":
            s, e = shape["start"], shape["end"]
            dx = insert_position["x"] - s[0]
            dy = insert_position["y"] - s[1]
            msp.add_line((s[0] + dx, s[1] + dy), (e[0] + dx, e[1] + dy))

        elif shape["type"] == "圆形":
            c = shape["center"]
            dx = insert_position["x"] - c[0]
            dy = insert_position["y"] - c[1]
            msp.add_circle((c[0] + dx, c[1] + dy), shape["radius"])

    # 插入关联文本（居中放置）
    if "关联文本" in target_row and isinstance(target_row["关联文本"], str):
        all_pts = [pt for shape in insert_shapes for pt in (
                shape.get("points") or [shape.get("start"), shape.get("end"), shape.get("center")]
        ) if pt]
        xs = [p[0] for p in all_pts if p]
        ys = [p[1] for p in all_pts if p]
        dx = insert_position["x"] - min(xs)
        dy = insert_position["y"] - min(ys)
        cx = min(xs) + dx + (max(xs) - min(xs)) / 2
        cy = min(ys) + dy + (max(ys) - min(ys)) / 2
        msp.add_text(target_row["关联文本"], dxfattribs={"height": 100}).dxf.insert = (cx, cy)

    doc.saveas(output_path)
    print(f"设备“{origin_device_name}”已替换为“{target_device_name}”，并保存至：{output_path}")

# ------------------------------------------------------------------------------------------------


def process_dxf_file(dxf_file_path):
    import os

    excel_path = r"E:\cadsecond-processing\drawingLibrary\原始设备库信息.xlsx"

    # 输出目录，确保存在
    output_excel_dir = r"E:\cadsecond-processing\output_excel"
    os.makedirs(output_excel_dir, exist_ok=True)  # ✅ 创建目录本身

    try:
        # 生成输出文件路径
        base_name = os.path.basename(dxf_file_path).replace(".dxf", "_shapes.xlsx")
        single_output_excel = os.path.join(output_excel_dir, base_name)

        # 执行图形提取和设备匹配
        dxf_json = extract_shapes_from_dxf(dxf_file_path, single_output_excel)
        df_result = merge_and_save_matched_devices(excel_path, dxf_json, single_output_excel)

        print(f"\n📄 {os.path.basename(dxf_file_path)} 匹配结果：\n", df_result.to_string(index=False))
        return single_output_excel, df_result, dxf_json
    except Exception as e:
        print(f"❌ 处理失败: {dxf_file_path}，错误: {e}")
        return None, None, None

def operate_original_dxf(excel_path_list,df_result_list,dxf_json_list,original_dxf_path,operation):
    updated_graph_dir = r"E:\cadsecond-processing\updated_graph"
    # 确保输出文件夹存在
    os.makedirs(updated_graph_dir, exist_ok=True)

    # 生成输出文件名（带后缀）
    base_name = os.path.basename(original_dxf_path).replace(".dxf", "")
    # 后续操作照常
    for idx, (df_result, dxf_json) in enumerate(zip(df_result_list, dxf_json_list), start=1):
        print(f"\n🚩 正在处理第 {idx} 个分图匹配结果 ...")
        if operation.get('mode') == 'add':
            add_device_to_dxf(
                dxf_file_path=original_dxf_path,
                match_df=df_result,
                dxf_json=dxf_json,
                output_path=os.path.join(updated_graph_dir, f"{base_name}_part{idx}_output_增加设备.dxf"),
                # target_device_name="壁挂空调",
                # insert_position={"x": 0, "y": 0},
                target_device_name=operation.get('target_device_name'),
                insert_position=operation.get('insert_position')
            )
        elif operation.get('mode') == 'delete':
            delete_device_from_dxf(
                dxf_file_path=original_dxf_path,
                match_df=df_result,
                dxf_json=dxf_json,
                output_path=os.path.join(updated_graph_dir, f"{base_name}_part{idx}_output_删除后图.dxf"),
                target_device_name=operation.get('target_device_name')
            )
        elif operation.get('mode') == 'move':
            move_device_in_dxf(
                dxf_file_path=original_dxf_path,
                match_df=df_result,
                dxf_json=dxf_json,
                output_path=os.path.join(updated_graph_dir, f"{base_name}_part{idx}_output_移动后图.dxf"),
                target_device_name=operation.get('target_device_name'),
                new_position=operation.get('target_device_name')
            )
        else:
            replace_device_in_dxf(
                dxf_file_path=original_dxf_path,
                match_df=df_result,
                dxf_json=dxf_json,
                output_path=os.path.join(updated_graph_dir, f"{base_name}_part{idx}_output_替换后图.dxf"),
                origin_device_name=operation.get('target_device_name'),
                target_device_name=operation.get('new_device_name')
            )

        print(f"✅ 第 {idx} 个分图操作完成！")

    return f"✅ 已完成对 {original_dxf_path} 的全部分图批量操作！"

# def filter_lists_by_excel_header(excel_path_list, df_result_list, dxf_json_list,operation):
#     """
#     检查每个 excel 的 '匹配结果' sheet 是否在第一行第一列包含 '设备类型'，
#     如果没有则从所有列表里移除该项。
#
#     返回：过滤后的 excel_path_list, df_result_list, dxf_json_list
#     """
#     keep_indexes = []
#     constraint = None
#     if operation.get('target_device_name') == '壁挂空调' \
#             or operation.get('target_device_name') == '铁塔机架' \
#             or operation.get('target_device_name') == '传输综合架':
#         constraint = '平面布置图'
#     for idx, excel_path in enumerate(excel_path_list):
#         try:
#             # 只读第一行第一列，且不把第一行作为列名
#             df = pd.read_excel(excel_path, sheet_name="匹配结果", header=None, nrows=1, usecols=[0])
#             first_cell = str(df.iloc[0, 0]).strip()
#             if first_cell == "设备类型":
#                 keep_indexes.append(idx)
#             else:
#                 print(f"⚠️ 文件 {excel_path} 的 '匹配结果' sheet 首列首单元格不是 '设备类型' (是 {first_cell})")
#         except Exception as e:
#             print(f"❌ 检查文件 {excel_path} 失败: {e}")
#
#     # 根据保留的索引筛选
#     new_excel_list = [excel_path_list[i] for i in keep_indexes]
#     new_df_result_list = [df_result_list[i] for i in keep_indexes]
#     new_dxf_json_list = [dxf_json_list[i] for i in keep_indexes]
#
#     print(f"\n✅ 保留 {len(new_excel_list)} 个有效结果，过滤掉 {len(excel_path_list) - len(new_excel_list)} 个")
#
#     return new_excel_list, new_df_result_list, new_dxf_json_list
def filter_lists_by_excel_header(excel_path_list, df_result_list, dxf_json_list, operation):
    """
    检查每个 excel 的 '匹配结果' sheet 是否在第一行第一列包含 '设备类型'，
    如果没有，或者（当有 constraint 时）路径不包含 constraint，则从所有列表里移除该项。

    返回：过滤后的 excel_path_list, df_result_list, dxf_json_list
    """
    keep_indexes = []

    # 判断是否需要约束
    constraint = None
    if operation.get('target_device_name') in ["壁挂空调", "铁塔机架", "传输综合架"]:
        constraint = "平面布置图"

    for idx, excel_path in enumerate(excel_path_list):
        try:
            df = pd.read_excel(excel_path, sheet_name="匹配结果", header=None, nrows=1, usecols=[0])
            first_cell = str(df.iloc[0, 0]).strip()

            if first_cell == "设备类型":
                if constraint:
                    if constraint in excel_path:
                        keep_indexes.append(idx)
                    else:
                        print(f"⚠️ 文件 {excel_path} 不包含约束 '{constraint}'，已过滤")
                else:
                    keep_indexes.append(idx)
            else:
                print(f"⚠️ 文件 {excel_path} 的 '匹配结果' sheet 首列首单元格不是 '设备类型' (是 {first_cell})")
        except Exception as e:
            print(f"❌ 检查文件 {excel_path} 失败: {e}")

    # 根据保留索引同步三个列表
    new_excel_list = [excel_path_list[i] for i in keep_indexes]
    new_df_result_list = [df_result_list[i] for i in keep_indexes]
    new_dxf_json_list = [dxf_json_list[i] for i in keep_indexes]

    print(f"\n✅ 保留 {len(new_excel_list)} 个有效结果，过滤掉 {len(excel_path_list) - len(new_excel_list)} 个")

    return new_excel_list, new_df_result_list, new_dxf_json_list


# if __name__ == "__main__":
    # origin_dxf_path = 'E:\cadsecond-processing\drawingLibrary\嘉兴海宁全1.dxf'
    # excel_path_list = []
    # df_result = []
    # dxf_json_resut = []
    # with ThreadPoolExecutor(max_workers=10) as executor:
    #     futures = [executor.submit(process_dxf_file, dxf) for dxf in dxf_files]
    #     for future in as_completed(futures):
    #         result = future.result()[0]
    #         df_result.append(future.result()[1])
    #         dxf_json_resut.append(future.result()[2])
    #         excel_path_list.append(result)
    # print(excel_path_list)
    # print(df_result)
    # print(dxf_json_resut)
    # excel_l,df_l,dxf_l = filter_lists_by_excel_header(excel_path_list,df_result,dxf_json_resut)
    # # print(filter_lists_by_excel_header(excel_path_list,df_result,dxf_json_resut))
    # print(f'Here is the excel:{excel_l}')
    # print("\n📂 所有分图结果收集完毕\n")
    # operate_original_dxf(excel_l,df_l,dxf_l, origin_dxf_path)